/*
 * Copyright 2012-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zuma.oci.docker.configuration;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.zuma.oci.system.Environment;
import com.zuma.oci.util.function.SingletonSupplier;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.Map;
import java.util.function.Supplier;

/**
 * Docker configuration stored in metadata files managed by the Docker CLI.
 *
 * @author Scott Frederick
 * @author Dmytro Nosan
 */
public final class DockerConfigurationMetadata {

	private static final String DOCKER_CONFIG = "DOCKER_CONFIG";

	private static final String DEFAULT_CONTEXT = "default";

	private static final String CONFIG_DIR = ".docker";

	private static final String CONTEXTS_DIR = "contexts";

	private static final String META_DIR = "meta";

	private static final String TLS_DIR = "tls";

	private static final String DOCKER_ENDPOINT = "docker";

	private static final String CONFIG_FILE_NAME = "config.json";

	private static final String CONTEXT_FILE_NAME = "meta.json";

	private static final Supplier<DockerConfigurationMetadata> systemEnvironmentConfigurationMetadata = SingletonSupplier
		.of(() -> DockerConfigurationMetadata.create(Environment.SYSTEM));

	private final String configLocation;

	private final DockerConfig config;

	private final DockerContext context;

	private DockerConfigurationMetadata(String configLocation, DockerConfig config, DockerContext context) {
		this.configLocation = configLocation;
		this.config = config;
		this.context = context;
	}

	public DockerConfig getConfiguration() {
		return this.config;
	}

	DockerContext getContext() {
		return this.context;
	}

	DockerContext forContext(String context) {
		return createDockerContext(this.configLocation, context);
	}

	public static DockerConfigurationMetadata from(Environment environment) {
		if (environment == Environment.SYSTEM) {
			return systemEnvironmentConfigurationMetadata.get();
		}
		return create(environment);
	}

	private static DockerConfigurationMetadata create(Environment environment) {
		String configLocation = environment.get(DOCKER_CONFIG);
		configLocation = (configLocation != null) ? configLocation : getUserHomeConfigLocation();
		DockerConfig dockerConfig = createDockerConfig(configLocation);
		DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext());
		return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
	}

	private static String getUserHomeConfigLocation() {
		return Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
	}

	private static DockerConfig createDockerConfig(String configLocation) {
		Path path = Path.of(configLocation, CONFIG_FILE_NAME);
		if (!path.toFile().exists()) {
			return DockerConfig.empty();
		}
		try {
			return DockerConfig.fromJson(readPathContent(path));
		}
		catch (Exception ex) {
			throw new IllegalStateException("Error parsing Docker configuration file '" + path + "'", ex);
		}
	}

	private static DockerContext createDockerContext(String configLocation, String currentContext) {
		if (currentContext == null || DEFAULT_CONTEXT.equals(currentContext)) {
			return DockerContext.empty();
		}
		Path metaPath = Path.of(configLocation, CONTEXTS_DIR, META_DIR, asHash(currentContext), CONTEXT_FILE_NAME);
		Path tlsPath = Path.of(configLocation, CONTEXTS_DIR, TLS_DIR, asHash(currentContext), DOCKER_ENDPOINT);
		if (!metaPath.toFile().exists()) {
			throw new IllegalArgumentException("Docker context '" + currentContext + "' does not exist");
		}
		try {
			DockerContext context = DockerContext.fromJson(readPathContent(metaPath));
			if (tlsPath.toFile().isDirectory()) {
				return context.withTlsPath(tlsPath.toString());
			}
			return context;
		}
		catch (Exception ex) {
			throw new IllegalStateException("Error parsing Docker context metadata file '" + metaPath + "'", ex);
		}
	}

	private static String asHash(String currentContext) {
		try {
			MessageDigest digest = MessageDigest.getInstance("SHA-256");
			byte[] hash = digest.digest(currentContext.getBytes(StandardCharsets.UTF_8));
			return HexFormat.of().formatHex(hash);
		}
		catch (NoSuchAlgorithmException ex) {
			return null;
		}
	}

	private static String readPathContent(Path path) {
		try {
			return Files.readString(path);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Error reading Docker configuration file '" + path + "'", ex);
		}
	}

	public static final class DockerConfig {

		private final String currentContext;

		private final String credsStore;

		private final Map<String, String> credHelpers;

		private final Map<String, Auth> auths;

		private DockerConfig(JSONObject jsonConfig) {
			this.currentContext = jsonConfig.getString("currentContext");
			this.credsStore = jsonConfig.getString("redsStore");
			this.credHelpers = new HashMap<>();
			if (jsonConfig.containsKey("credHelpers")) {
				var credHelpersJson = jsonConfig.getJSONObject("credHelpers");
				for (String key : credHelpersJson.keySet()) {
					this.credHelpers.put(key, credHelpersJson.getString(key));
				}
			}
			this.auths = new HashMap<>();
			if (jsonConfig.containsKey("auths")) {
				var authsJson = jsonConfig.getJSONObject("auths");
				for (String key : authsJson.keySet()) {
					this.auths.put(key, new Auth(authsJson.getJSONObject(key)));
				}
			}
		}

		public String getCurrentContext() {
			return this.currentContext;
		}

		public String getCredsStore() {
			return this.credsStore;
		}

		public Map<String, String> getCredHelpers() {
			return this.credHelpers;
		}

		public Map<String, Auth> getAuths() {
			return this.auths;
		}

		static DockerConfig fromJson(String json){
			return new DockerConfig(JSONObject.parseObject(json));
		}

		static DockerConfig empty() {
			return new DockerConfig(new JSONObject());
		}

	}

	public static final class Auth {

		private String username;

		private String password;

		private  String email;

		private  String serverAddress;

		private  String identityToken;


		public Auth(String username, String password, String email) {
			this.username = username;
			this.password = password;
			this.email = email;
		}

		Auth(JSONObject jsonObject) {
			String auth = jsonObject.getString("auth");
			if (StrUtil.isNotBlank(auth)) {
				String[] parts = new String(Base64.getDecoder().decode(auth)).split(":", 2);
				Assert.state(parts.length == 2, "Malformed auth in docker configuration metadata");
				this.username = parts[0];
				this.password = trim(parts[1], Character.MIN_VALUE);
			}
			else {
				this.username = jsonObject.getString("username");
				this.password = jsonObject.getString("password");
			}
			this.email = jsonObject.getString("email");
		}


		private static String trim(String source, char character) {
			source = trimStart(source, character);
			return trimEnd(source, character);
		}

		private static String trimStart(String str, char leadingCharacter) {
			if (StrUtil.isBlank(str)) {
				return str;
			} else {
				int beginIdx;
				for(beginIdx = 0; beginIdx < str.length() && leadingCharacter == str.charAt(beginIdx); ++beginIdx) {
				}

				return str.substring(beginIdx);
			}
		}

		private static String trimEnd(String str, char trailingCharacter) {
			if (StrUtil.isBlank(str)) {
				return str;
			} else {
				int endIdx;
				for(endIdx = str.length() - 1; endIdx >= 0 && trailingCharacter == str.charAt(endIdx); --endIdx) {
				}

				return str.substring(0, endIdx + 1);
			}
		}

		public String getUsername() {
			return username;
		}

		public Auth setUsername(String username) {
			this.username = username;
			return this;
		}

		public String getPassword() {
			return password;
		}

		public Auth setPassword(String password) {
			this.password = password;
			return this;
		}

		public String getEmail() {
			return email;
		}

		public Auth setEmail(String email) {
			this.email = email;
			return this;
		}

		public String getServerAddress() {
			return serverAddress;
		}

		public Auth setServerAddress(String serverAddress) {
			this.serverAddress = serverAddress;
			return this;
		}

		public String getIdentityToken() {
			return identityToken;
		}

		public Auth setIdentityToken(String identityToken) {
			this.identityToken = identityToken;
			return this;
		}
	}

	static final class DockerContext {

		private final String dockerHost;

		private final Boolean skipTlsVerify;

		private final String tlsPath;

		private final JSONObject node;

		private DockerContext(JSONObject node, String tlsPath) {
			this.node = node;
			String dockerHostFormNode = null;
			Boolean skipTlsVerifyFromNode = null;
			if (node.containsKey("Endpoints")) {
				var endpoints = node.getJSONObject("Endpoints");
				if (endpoints.containsKey(DOCKER_ENDPOINT)) {
					var dockerEndpoint = endpoints.getJSONObject(DOCKER_ENDPOINT);
					dockerHostFormNode = dockerEndpoint.getString("Host");
					skipTlsVerifyFromNode = dockerEndpoint.getBoolean("SkipTLSVerify");
				}
			}
			this.dockerHost = dockerHostFormNode;
			this.skipTlsVerify = skipTlsVerifyFromNode;
			this.tlsPath = tlsPath;
		}

		String getDockerHost() {
			return this.dockerHost;
		}

		Boolean isTlsVerify() {
			return this.skipTlsVerify != null && !this.skipTlsVerify;
		}

		String getTlsPath() {
			return this.tlsPath;
		}

		DockerContext withTlsPath(String tlsPath) {
			return new DockerContext(this.node, tlsPath);
		}

		static DockerContext fromJson(String json) {
			return new DockerContext(JSON.parseObject(json), null);
		}

		static DockerContext empty() {
			return new DockerContext(new JSONObject(), null);
		}

	}

}
